﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using K9Nano.IM.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using MySql.Data.MySqlClient;
using Polly.Caching;
using WalkingTec.Mvvm.Core;

namespace K9Nano.IM
{
    public class DataContext : FrameworkContext
    {
        public DataContext(CS cs)
             : base(cs)
        {
        }

        public DataContext(string cs, DBTypeEnum dbtype, string version = null)
             : base(cs, dbtype, version)
        {
        }

        public virtual DbSet<User> Users { get; set; }
        public virtual DbSet<ChatMessage> ChatMessages { get; set; }
        public virtual DbSet<ChatSession> ChatSessions { get; set; }
        public virtual DbSet<ChatApp> ChatApps { get; set; }
        public virtual DbSet<ChatGroup> ChatGroups { get; set; }
        public virtual DbSet<ChatGroupMember> ChatGroupMembers { get; set; }
        public virtual DbSet<ChatRoom> ChatRooms { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            var user = modelBuilder.Entity<User>();
            user.HasIndex(x => new { x.AppId, x.ExternalId })
                .HasName("idx_unique_app");

            var message = modelBuilder.Entity<ChatMessage>();
            message.HasIndex(x => x.SessionId)
                .HasName("idx_session");
            message.HasIndex(x => x.FromId)
                .HasName("idx_from");
            message.HasIndex(x => x.ToId)
                .HasName("idx_to");

            var session = modelBuilder.Entity<ChatSession>();
            session.HasIndex(x => new { x.AppId, x.CustomerId })
                .HasName("idx_app_customer");
            session.HasIndex(x => x.StaffId)
                .HasName("idx_staff");

            var group = modelBuilder.Entity<ChatGroup>();
            group.HasIndex(x => new { x.OwnerId, x.AppId })
                .IsUnique()
                .HasName("idx_owner_app");

            var groupMember = modelBuilder.Entity<ChatGroupMember>();
            groupMember.HasIndex(x => new { x.GroupId, x.FriendId })
                .IsUnique()
                .HasName("idx_group_member");

            var room = modelBuilder.Entity<ChatRoom>();
            room.HasIndex(x => new { x.CustomerId, x.AppId })
                .IsUnique()
                .HasName("idx_customer_app");

            var roomMember = modelBuilder.Entity<ChatRoomMember>();
            roomMember.HasIndex(x => new { x.RoomId, x.MemberId })
                .IsUnique()
                .HasName("idx_room_member");
        }

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            BeforeSaveChanges();

            return base.SaveChanges(acceptAllChangesOnSuccess);
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
        {
            BeforeSaveChanges();
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }

        private void BeforeSaveChanges()
        {
            var entries = this.ChangeTracker.Entries();

            foreach (var entry in entries)
            {
                if (entry.State == EntityState.Added)
                {
                    if (entry.Entity is TopBasePoco)
                    {
                        var nextId = NextGuid();
                        entry.CurrentValues[nameof(TopBasePoco.ID)] = nextId;
                    }
                }
            }
        }

        private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();

        public static Guid NextGuid()
        {
            // According to RFC 4122:
            // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
            // - M = RFC version, in this case '4' for random UUID
            // - N = RFC variant (plus other bits), in this case 0b1000 for variant 1
            // - d = nibbles based on UTC date/time in ticks
            // - r = nibbles based on random bytes

            var randomBytes = new byte[7];
            _rng.GetBytes(randomBytes);
            var ticks = (ulong)DateTimeOffset.Now.Ticks;

            var uuidVersion = (ushort)4;
            var uuidVariant = (ushort)0b1000;

            var ticksAndVersion = (ushort)((ticks << 48 >> 52) | (ushort)(uuidVersion << 12));
            var ticksAndVariant = (byte)((ticks << 60 >> 60) | (byte)(uuidVariant << 4));
            
            var guid = new Guid((uint)(ticks >> 32), (ushort)(ticks << 32 >> 48), ticksAndVersion,
                ticksAndVariant,
                randomBytes[0],
                randomBytes[1],
                randomBytes[2],
                randomBytes[3],
                randomBytes[4],
                randomBytes[5],
                randomBytes[6]);

            return guid;
        }
    }

    /// <summary>
    /// DesignTimeFactory for EF Migration, use your full connection string,
    /// EF will find this class and use the connection defined here to run Add-Migration and Update-Database
    /// </summary>
    public class DataContextFactory : IDesignTimeDbContextFactory<DataContext>
    {
        public DataContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                //.AddJsonFile(Path.Combine("config", "appsettings.json"))
                .Build();

            var connectionStrings = new List<KV>();
            configuration.GetSection("ConnectionStrings").Bind(connectionStrings);
            var first = connectionStrings[0].Value;
            Console.WriteLine($"ConnectionString: {first}");
            return new DataContext(first, DBTypeEnum.MySql);
        }
    }

}
